use iced::alignment::Horizontal;
use iced::highlighter;
use iced::widget::text::{Shaping, Wrapping};
use iced::widget::{
    Column, Row, button, column, container, row, scrollable, space, text,
    text_editor, text_input,
};
use iced::{
    Background, Color, Element, Fill, Font, Length, Shadow, Task, Theme, border,
};

use directories::ProjectDirs;
use serde::{Deserialize, Serialize};

use std::fs;
use std::io::ErrorKind;
use std::path::PathBuf;

const INITIAL_SNIPPET: &str = "// Welcome to the AI editor example!\n// Type on the right, chat on the left.\n";
const STORAGE_FILE: &str = "state.json";

type ConversationId = u64;

pub fn main() -> iced::Result {
    iced::application(AiEditor::new, AiEditor::update, AiEditor::view)
        .title("AI Editor")
        .default_font(Font::MONOSPACE)
        .theme(AiEditor::theme)
        .run()
}

struct AiEditor {
    content: text_editor::Content,
    conversations: Vec<Conversation>,
    active_conversation: usize,
    content_revision: u64,
    next_request_id: u64,
    next_conversation_id: ConversationId,
    has_loaded: bool,
}

#[derive(Debug, Clone)]
struct Conversation {
    id: ConversationId,
    title: String,
    messages: Vec<ChatMessage>,
    chat_input: String,
    pending_chat: Option<u64>,
    prediction: Option<Prediction>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatMessage {
    role: Role,
    text: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum Role {
    User,
    Assistant,
    System,
}

#[derive(Debug, Clone)]
struct Prediction {
    id: u64,
    revision: u64,
    state: PredictionState,
}

#[derive(Debug, Clone)]
enum PredictionState {
    Loading,
    Ready(AiSuggestion),
    Failed(String),
}

#[derive(Debug, Clone)]
struct AiSuggestion {
    title: String,
    explanation: String,
    snippet: String,
}

#[derive(Debug, Clone)]
struct AiChatResult {
    reply: String,
    suggestion: Option<AiSuggestion>,
}

#[derive(Debug, Clone)]
struct AiError {
    reason: String,
}

#[derive(Debug, Clone)]
enum Message {
    EditorAction(text_editor::Action),
    ChatInputChanged(ConversationId, String),
    SubmitChat(ConversationId),
    AiResponded(ConversationId, u64, Result<AiChatResult, AiError>),
    RequestPrediction(ConversationId),
    PredictionReady(
        ConversationId,
        u64,
        u64,
        Result<Option<AiSuggestion>, AiError>,
    ),
    DismissPrediction(ConversationId),
    ApplySuggestion(ConversationId),
    AddConversation,
    SelectConversation(ConversationId),
    StateLoaded(Result<Option<PersistedState>, PersistenceError>),
    StateSaved(Result<(), PersistenceError>),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedState {
    content: String,
    conversations: Vec<ConversationState>,
    active_conversation_id: Option<ConversationId>,
    content_revision: u64,
    next_request_id: u64,
    next_conversation_id: ConversationId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ConversationState {
    id: ConversationId,
    title: String,
    messages: Vec<ChatMessage>,
    chat_input: String,
}

#[derive(Debug, Clone)]
struct PersistenceError {
    message: String,
}

impl PersistenceError {
    fn unavailable() -> Self {
        Self {
            message: "Persistent storage is not available on this platform."
                .into(),
        }
    }

    fn io(error: std::io::Error) -> Self {
        Self {
            message: format!("I/O error: {}", error),
        }
    }

    fn format(error: serde_json::Error) -> Self {
        Self {
            message: format!("Could not parse saved state: {}", error),
        }
    }
}

impl std::fmt::Display for PersistenceError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for PersistenceError {}

impl AiEditor {
    fn new() -> (Self, Task<Message>) {
        let first_id = 1;

        let editor = Self {
            content: text_editor::Content::with_text(INITIAL_SNIPPET),
            conversations: vec![Conversation::new(first_id, "Chat 1".into())],
            active_conversation: 0,
            content_revision: 0,
            next_request_id: 1,
            next_conversation_id: first_id + 1,
            has_loaded: false,
        };

        let load = Task::perform(load_state(), Message::StateLoaded);

        (editor, load)
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::EditorAction(action) => {
                let mut tasks = Vec::new();
                let is_edit = action.is_edit();

                if is_edit {
                    self.content_revision =
                        self.content_revision.wrapping_add(1);

                    for conversation in &mut self.conversations {
                        conversation.prediction = None;
                    }
                }

                self.content.perform(action);

                if is_edit {
                    if let Some(save) = self.maybe_save_task() {
                        tasks.push(save);
                    }
                }

                Self::batch_tasks(tasks)
            }
            Message::ChatInputChanged(id, value) => {
                if let Some(conversation) = self.conversation_mut(id) {
                    conversation.chat_input = value;

                    if let Some(save) = self.maybe_save_task() {
                        return save;
                    }
                }

                Task::none()
            }
            Message::SubmitChat(id) => {
                let Some(index) = self.conversation_index(id) else {
                    return Task::none();
                };

                if self.conversations[index].pending_chat.is_some() {
                    return Task::none();
                }

                let prompt =
                    self.conversations[index].chat_input.trim().to_owned();

                if prompt.is_empty() {
                    return Task::none();
                }

                let context = self.content.text();
                let request_id = self.next_request_id;
                self.next_request_id =
                    self.next_request_id.wrapping_add(1).max(1);

                {
                    let conversation = &mut self.conversations[index];
                    conversation.chat_input.clear();
                    conversation.messages.push(ChatMessage {
                        role: Role::User,
                        text: prompt.clone(),
                    });
                    conversation.pending_chat = Some(request_id);
                    conversation.prediction = None;
                }

                let mut tasks = vec![Task::perform(
                    AiEngine::chat(prompt, context),
                    move |result| Message::AiResponded(id, request_id, result),
                )];

                if let Some(save) = self.maybe_save_task() {
                    tasks.push(save);
                }

                Self::batch_tasks(tasks)
            }
            Message::AiResponded(id, request_id, result) => {
                let revision = self.content_revision;
                let Some(conversation) = self.conversation_mut(id) else {
                    return Task::none();
                };

                if conversation.pending_chat != Some(request_id) {
                    return Task::none();
                }

                conversation.pending_chat = None;

                match result {
                    Ok(output) => {
                        conversation.messages.push(ChatMessage {
                            role: Role::Assistant,
                            text: output.reply,
                        });

                        if let Some(suggestion) = output.suggestion {
                            conversation.prediction = Some(Prediction {
                                id: request_id,
                                revision,
                                state: PredictionState::Ready(suggestion),
                            });
                        }
                    }
                    Err(error) => {
                        conversation.messages.push(ChatMessage {
                            role: Role::System,
                            text: format!("Assistant failed: {}", error.reason),
                        });
                    }
                }

                if let Some(save) = self.maybe_save_task() {
                    save
                } else {
                    Task::none()
                }
            }
            Message::RequestPrediction(id) => {
                let Some(index) = self.conversation_index(id) else {
                    return Task::none();
                };

                let revision = self.content_revision;
                let context = self.content.text();
                let request_id = self.next_request_id;
                self.next_request_id =
                    self.next_request_id.wrapping_add(1).max(1);

                self.conversations[index].prediction = Some(Prediction {
                    id: request_id,
                    revision,
                    state: PredictionState::Loading,
                });

                Task::perform(AiEngine::predict(context), move |result| {
                    Message::PredictionReady(id, request_id, revision, result)
                })
            }
            Message::PredictionReady(id, request_id, revision, result) => {
                let Some(conversation) = self.conversation_mut(id) else {
                    return Task::none();
                };

                let Some(prediction) = conversation.prediction.as_mut() else {
                    return Task::none();
                };

                if prediction.id != request_id
                    || prediction.revision != revision
                {
                    return Task::none();
                }

                prediction.state = match result {
                    Ok(Some(suggestion)) => PredictionState::Ready(suggestion),
                    Ok(None) => PredictionState::Failed(
                        "No useful suggestion found.".into(),
                    ),
                    Err(error) => PredictionState::Failed(format!(
                        "Prediction failed: {}",
                        error.reason
                    )),
                };

                Task::none()
            }
            Message::DismissPrediction(id) => {
                if let Some(conversation) = self.conversation_mut(id) {
                    conversation.prediction = None;
                }

                Task::none()
            }
            Message::ApplySuggestion(id) => {
                let Some(conversation) = self.conversation_mut(id) else {
                    return Task::none();
                };

                let snippet = match conversation.prediction.as_ref() {
                    Some(Prediction {
                        state: PredictionState::Ready(suggestion),
                        ..
                    }) => suggestion.snippet.clone(),
                    _ => return Task::none(),
                };

                conversation.prediction = None;
                self.content.perform(text_editor::Action::Edit(
                    text_editor::Edit::Paste(snippet.into()),
                ));
                self.content_revision = self.content_revision.wrapping_add(1);

                if let Some(save) = self.maybe_save_task() {
                    save
                } else {
                    Task::none()
                }
            }
            Message::AddConversation => {
                let id = self.next_conversation_id;
                self.next_conversation_id =
                    self.next_conversation_id.wrapping_add(1).max(1);
                let title = self.new_conversation_title();
                self.conversations.push(Conversation::new(id, title));
                self.active_conversation = self.conversations.len() - 1;

                if let Some(save) = self.maybe_save_task() {
                    save
                } else {
                    Task::none()
                }
            }
            Message::SelectConversation(id) => {
                if let Some(index) = self.conversation_index(id) {
                    self.active_conversation = index;

                    if let Some(save) = self.maybe_save_task() {
                        return save;
                    }
                }

                Task::none()
            }
            Message::StateLoaded(result) => {
                match result {
                    Ok(Some(state)) => self.apply_state(state),
                    Ok(None) => {}
                    Err(error) => self.push_system_message(format!(
                        "Failed to restore previous session: {}",
                        error
                    )),
                }

                self.has_loaded = true;

                Task::none()
            }
            Message::StateSaved(result) => {
                if let Err(error) = result {
                    self.push_system_message(format!(
                        "Could not save session: {}",
                        error
                    ));
                }

                Task::none()
            }
        }
    }

    fn view(&self) -> Element<'_, Message> {
        let Some(conversation) =
            self.conversations.get(self.active_conversation)
        else {
            return container(
                column![
                    text("No conversation available").size(20),
                    button("Start chatting").on_press(Message::AddConversation),
                ]
                .spacing(12),
            )
            .width(Length::Fill)
            .height(Length::Fill)
            .into();
        };

        let conversation_id = conversation.id;

        let mut tabs: Row<Message> = Row::new().spacing(8);

        for conv in &self.conversations {
            let style = if conv.id == conversation_id {
                button::primary
            } else {
                button::secondary
            };

            tabs = tabs.push(
                button(text(&conv.title).size(16).shaping(Shaping::Advanced))
                    .padding([4, 12])
                    .style(style)
                    .on_press(Message::SelectConversation(conv.id)),
            );
        }

        tabs = tabs.push(space::horizontal()).push(
            button(text("+").size(16))
                .padding([4, 12])
                .on_press(Message::AddConversation),
        );

        let tab_bar: Element<'_, Message> = tabs.into();

        let chat_history = conversation
            .messages
            .iter()
            .fold(Column::new().spacing(12), |column, message| {
                column.push(chat_bubble(message))
            });

        let send_enabled = conversation.pending_chat.is_none()
            && !conversation.chat_input.trim().is_empty();

        let chat_panel = column![
            tab_bar,
            scrollable(chat_history).height(Fill),
            row![
                text_input("Send a message", &conversation.chat_input)
                    .on_input(move |value| Message::ChatInputChanged(
                        conversation_id,
                        value
                    ))
                    .on_submit(Message::SubmitChat(conversation_id)),
                button(if conversation.pending_chat.is_some() {
                    text("...")
                } else {
                    text("Send")
                })
                .on_press_maybe(
                    send_enabled
                        .then_some(Message::SubmitChat(conversation_id))
                ),
            ]
            .spacing(8),
        ]
        .spacing(16)
        .height(Fill)
        .padding([4, 8]);

        let editor = text_editor(&self.content)
            .height(Fill)
            .on_action(Message::EditorAction)
            .highlight("rs", highlighter::Theme::SolarizedDark)
            .wrapping(Wrapping::Word);

        let prediction_button = {
            let busy =
                conversation.prediction.as_ref().is_some_and(|prediction| {
                    matches!(prediction.state, PredictionState::Loading)
                });

            button("Ask for suggestion").on_press_maybe(
                (!busy).then_some(Message::RequestPrediction(conversation_id)),
            )
        };

        let editor_panel = column![
            text("Editor").size(22).shaping(Shaping::Advanced),
            editor,
            prediction_button,
            prediction_panel(conversation_id, conversation.prediction.as_ref()),
        ]
        .spacing(16)
        .height(Fill)
        .padding([4, 8]);

        row![
            chat_panel.width(Length::FillPortion(5)),
            editor_panel.width(Length::FillPortion(7)),
        ]
        .spacing(24)
        .padding(20)
        .height(Fill)
        .into()
    }

    fn theme(&self) -> Theme {
        Theme::Dark
    }

    fn conversation_index(&self, id: ConversationId) -> Option<usize> {
        self.conversations
            .iter()
            .position(|conversation| conversation.id == id)
    }

    fn conversation_mut(
        &mut self,
        id: ConversationId,
    ) -> Option<&mut Conversation> {
        let index = self.conversation_index(id)?;
        self.conversations.get_mut(index)
    }

    fn maybe_save_task(&self) -> Option<Task<Message>> {
        if self.has_loaded {
            Some(self.save_task())
        } else {
            None
        }
    }

    fn save_task(&self) -> Task<Message> {
        let snapshot = self.snapshot();
        Task::perform(save_state(snapshot), Message::StateSaved)
    }

    fn snapshot(&self) -> PersistedState {
        PersistedState {
            content: self.content.text(),
            conversations: self
                .conversations
                .iter()
                .map(ConversationState::from)
                .collect(),
            active_conversation_id: self
                .conversations
                .get(self.active_conversation)
                .map(|conversation| conversation.id),
            content_revision: self.content_revision,
            next_request_id: self.next_request_id.max(1),
            next_conversation_id: self.next_conversation_id.max(1),
        }
    }

    fn apply_state(&mut self, state: PersistedState) {
        self.content = text_editor::Content::with_text(&state.content);
        self.content_revision = state.content_revision;
        self.next_request_id = state.next_request_id.max(1);
        self.next_conversation_id = state.next_conversation_id.max(1);

        self.conversations = state
            .conversations
            .into_iter()
            .map(Conversation::from_state)
            .collect();

        if self.conversations.is_empty() {
            let id = self.next_conversation_id;
            self.next_conversation_id =
                self.next_conversation_id.wrapping_add(1).max(1);
            self.conversations
                .push(Conversation::new(id, "Chat 1".into()));
        }

        let max_id = self
            .conversations
            .iter()
            .map(|conversation| conversation.id)
            .max()
            .unwrap_or(0);
        if self.next_conversation_id <= max_id {
            self.next_conversation_id = max_id + 1;
        }

        if let Some(active_id) = state.active_conversation_id {
            if let Some(index) = self.conversation_index(active_id) {
                self.active_conversation = index;
            } else {
                self.active_conversation = 0;
            }
        } else {
            self.active_conversation = 0;
        }
    }

    fn push_system_message(&mut self, text: String) {
        if self.conversations.is_empty() {
            let id = self.next_conversation_id;
            self.next_conversation_id =
                self.next_conversation_id.wrapping_add(1).max(1);
            self.conversations
                .push(Conversation::new(id, "Chat 1".into()));
            self.active_conversation = 0;
        }

        let index = self.active_conversation.min(self.conversations.len() - 1);
        self.active_conversation = index;

        if let Some(conversation) = self.conversations.get_mut(index) {
            conversation.messages.push(ChatMessage {
                role: Role::System,
                text,
            });
        }
    }

    fn new_conversation_title(&self) -> String {
        format!("Chat {}", self.conversations.len() + 1)
    }

    fn batch_tasks(tasks: Vec<Task<Message>>) -> Task<Message> {
        match tasks.len() {
            0 => Task::none(),
            1 => tasks.into_iter().next().unwrap(),
            _ => Task::batch(tasks),
        }
    }
}

impl Conversation {
    fn new(id: ConversationId, title: String) -> Self {
        Self {
            id,
            title,
            messages: vec![ChatMessage {
                role: Role::Assistant,
                text: "Hi! Ask me about the code and I will share suggestions."
                    .into(),
            }],
            chat_input: String::new(),
            pending_chat: None,
            prediction: None,
        }
    }

    fn from_state(state: ConversationState) -> Self {
        Self {
            id: state.id,
            title: state.title,
            messages: state.messages,
            chat_input: state.chat_input,
            pending_chat: None,
            prediction: None,
        }
    }
}

impl From<&Conversation> for ConversationState {
    fn from(conversation: &Conversation) -> Self {
        Self {
            id: conversation.id,
            title: conversation.title.clone(),
            messages: conversation.messages.clone(),
            chat_input: conversation.chat_input.clone(),
        }
    }
}

fn chat_bubble(message: &ChatMessage) -> Element<'_, Message> {
    let label = match message.role {
        Role::User => "You",
        Role::Assistant => "Assistant",
        Role::System => "System",
    };

    let align = match message.role {
        Role::User => Horizontal::Right,
        Role::Assistant => Horizontal::Left,
        Role::System => Horizontal::Center,
    };

    let content = column![
        text(label).size(14),
        text(&message.text)
            .size(16)
            .wrapping(Wrapping::Word)
            .shaping(Shaping::Advanced),
    ]
    .spacing(6);

    container(content)
        .padding([8, 12])
        .align_x(align)
        .width(Length::Fill)
        .style(move |theme: &Theme| {
            let palette = theme.extended_palette();
            let (background, text_color) = match message.role {
                Role::User => {
                    (palette.primary.weak.color, palette.primary.strong.text)
                }
                Role::Assistant => (
                    palette.background.strong.color,
                    palette.background.strong.text,
                ),
                Role::System => {
                    (palette.danger.weak.color, palette.danger.strong.text)
                }
            };

            container::Style {
                text_color: Some(text_color),
                background: Some(Background::Color(background)),
                border: border::Border {
                    radius: 10.0.into(),
                    ..Default::default()
                },
                shadow: Shadow::default(),
                snap: true,
            }
        })
        .into()
}

fn prediction_panel(
    conversation_id: ConversationId,
    prediction: Option<&Prediction>,
) -> Element<'_, Message> {
    match prediction {
        None => container(Column::new())
            .height(Length::Shrink)
            .width(Length::Fill)
            .into(),
        Some(Prediction {
            state: PredictionState::Loading,
            ..
        }) => container(text("Generating suggestion...").size(16))
            .padding(12)
            .style(container::rounded_box)
            .width(Length::Fill)
            .into(),
        Some(Prediction {
            state: PredictionState::Failed(reason),
            ..
        }) => container(
            column![
                text("No suggestion available")
                    .size(16)
                    .color(Color::from_rgb(0.9, 0.4, 0.4)),
                text(reason).size(14),
                button("Dismiss")
                    .on_press(Message::DismissPrediction(conversation_id)),
            ]
            .spacing(10),
        )
        .padding(12)
        .style(container::rounded_box)
        .width(Length::Fill)
        .into(),
        Some(Prediction {
            state: PredictionState::Ready(suggestion),
            ..
        }) => {
            let snippet = container(
                text(&suggestion.snippet)
                    .font(Font::MONOSPACE)
                    .size(14)
                    .wrapping(Wrapping::Word),
            )
            .padding(8)
            .style(container::rounded_box);

            container(
                column![
                    text(&suggestion.title).size(18).shaping(Shaping::Advanced),
                    text(&suggestion.explanation).size(14),
                    snippet,
                    row![
                        button("Apply suggestion").on_press(
                            Message::ApplySuggestion(conversation_id)
                        ),
                        button("Dismiss").on_press(Message::DismissPrediction(
                            conversation_id
                        )),
                    ]
                    .spacing(8),
                ]
                .spacing(10),
            )
            .padding(12)
            .style(container::rounded_box)
            .width(Length::Fill)
            .into()
        }
    }
}

struct AiEngine;

impl AiEngine {
    fn chat(
        prompt: String,
        context: String,
    ) -> impl std::future::Future<Output = Result<AiChatResult, AiError>> {
        iced::futures::future::ready({
            let reply = Self::chat_reply(&prompt, &context);
            let suggestion = Self::prompt_suggestion(&prompt, &context)
                .or_else(|| Self::context_suggestion(&context));

            Ok(AiChatResult { reply, suggestion })
        })
    }

    fn predict(
        context: String,
    ) -> impl std::future::Future<Output = Result<Option<AiSuggestion>, AiError>>
    {
        iced::futures::future::ready(Ok(Self::context_suggestion(&context)))
    }

    fn chat_reply(prompt: &str, context: &str) -> String {
        let lower = prompt.to_lowercase();

        if lower.contains("hello") || lower.contains("hi") {
            "Hello! Let's work on the code together.".into()
        } else if lower.contains("refactor") {
            "Consider extracting reusable logic into helper functions.".into()
        } else if lower.contains("test") {
            "We can scaffold a test module to validate the behavior.".into()
        } else if lower.contains("async") {
            "Remember to drive async code with an executor and propagate errors.".into()
        } else if context.trim().is_empty() {
            "The editor is blank, maybe start with a main function?".into()
        } else {
            format!(
                "I have {} lines to look at. What would you like to add?",
                context.lines().count()
            )
        }
    }

    fn prompt_suggestion(prompt: &str, context: &str) -> Option<AiSuggestion> {
        let lower = prompt.to_lowercase();

        if lower.contains("main") && !context.contains("fn main") {
            Some(AiSuggestion {
                title: "Add an entry point".into(),
                explanation: "The buffer is missing `fn main`. Here is a small starting point.".into(),
                snippet: "fn main() {\n    println!(\"Hello from the AI editor!\");\n}\n".into(),
            })
        } else if lower.contains("test") {
            Some(AiSuggestion {
                title: "Create a regression test".into(),
                explanation: "A basic `#[cfg(test)]` module keeps behavior covered.".into(),
                snippet: "#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn it_works() {\n        assert_eq!(2 + 2, 4);\n    }\n}\n".into(),
            })
        } else if lower.contains("struct") {
            Some(AiSuggestion {
                title: "Stub an `impl` block".into(),
                explanation: "Implementing methods next to the struct keeps things organized.".into(),
                snippet: "impl MyType {\n    pub fn new() -> Self {\n        Self {}\n    }\n}\n".into(),
            })
        } else {
            None
        }
    }

    fn context_suggestion(context: &str) -> Option<AiSuggestion> {
        if !context.contains("fn main") {
            return Some(AiSuggestion {
                title: "Bootstrap a main function".into(),
                explanation: "No `fn main` was detected, so here is a template entry point.".into(),
                snippet: "fn main() {\n    println!(\"Welcome to the AI editor example!\");\n}\n".into(),
            });
        }

        let last_non_empty = context
            .lines()
            .rev()
            .find(|line| !line.trim().is_empty())
            .map(|line| line.trim());

        if let Some(line) = last_non_empty {
            if line.starts_with("impl") && !context.contains("fn new") {
                return Some(AiSuggestion {
                    title: "Provide a constructor".into(),
                    explanation: "The `impl` block can expose a `new` function to simplify instantiation.".into(),
                    snippet: "    pub fn new() -> Self {\n        Self {}\n    }\n".into(),
                });
            }

            if line.starts_with("pub struct") && !context.contains("impl ") {
                return Some(AiSuggestion {
                    title: "Add an implementation block".into(),
                    explanation: "An `impl` block unlocks methods on the struct you just declared.".into(),
                    snippet: "impl Example {\n    pub fn new() -> Self {\n        Self {}\n    }\n}\n".into(),
                });
            }
        }

        None
    }
}

fn storage_path() -> Option<PathBuf> {
    ProjectDirs::from("org", "iced", "ai_editor")
        .map(|dirs| dirs.data_dir().join(STORAGE_FILE))
}

fn load_state() -> impl std::future::Future<
    Output = Result<Option<PersistedState>, PersistenceError>,
> {
    async move {
        let Some(path) = storage_path() else {
            return Err(PersistenceError::unavailable());
        };

        let contents = match fs::read_to_string(&path) {
            Ok(data) => data,
            Err(error) if error.kind() == ErrorKind::NotFound => {
                return Ok(None);
            }
            Err(error) => return Err(PersistenceError::io(error)),
        };

        let state = serde_json::from_str(&contents)
            .map_err(PersistenceError::format)?;

        Ok(Some(state))
    }
}

fn save_state(
    state: PersistedState,
) -> impl std::future::Future<Output = Result<(), PersistenceError>> {
    async move {
        let Some(path) = storage_path() else {
            return Err(PersistenceError::unavailable());
        };

        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(PersistenceError::io)?;
        }

        let payload = serde_json::to_string_pretty(&state)
            .map_err(PersistenceError::format)?;

        fs::write(path, payload).map_err(PersistenceError::io)
    }
}
